iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

講完 DX 那端的 Compute pipeline 設定後,這篇就是正式要開始用到 Compute Shader 了。

前面有提到 StructuredBuffer,是可以把大量格式相同的資料一次傳進 GPU 來處理 Draw call 問題的作法。

不過其實StructuredBuffer 是唯讀的, Shader 無法去修改裡面的值。像是如果要傳粒子的資料,然後他要是可以位移的,就不能用 StructuredBuffer 了。

struct Particle
{
		float3 position; //像是這種就無法更改
		float3 velocity;
};

但今天我還是希望可以在 GPU 計算出位置後去更改 position 的話,就會用到另一個可以讀寫的版本,也就是 RWStructuredBuffer

// HLSL 寫法
RWStructuredBuffer<Particle> particles : register(u0)

實作

//particle_common.hlsli

#ifndef PARTICLE_COMMON_HLSLI
#define PARTICLE_COMMON_HLSLI

// 定義 Compute Shader 與 Render Shader 共用的粒子資料格式
struct Particle
{
    // 儲存粒子在世界空間中的位置
    float3 position;

    // 儲存粒子的剩餘生命時間
    float life;
    
    // 儲存粒子的移動方向
    float3 velocity;

    // 保留欄位以維持結構的記憶體對齊
    float padding;
};

#endif
//particle_compute.hlsl

#include "particle_common.hlsli"

// 宣告可由 Compute Shader 讀寫的粒子 StructuredBuffer 並綁定至 u0
RWStructuredBuffer<Particle> particles : register(u0);

// 宣告粒子模擬使用的常數緩衝區並綁定至 b0
cbuffer ParticleSimulationConstants : register(b0)
{
    // 儲存目前 Frame 經過的時間
    float deltaTime;

    // 儲存目前實際使用的粒子數量
    uint particleCount;

    // 儲存粒子 Buffer 可容納的最大粒子數量
    uint particleCapacity;

    // 儲存粒子活動空間的邊界大小
    float boundarySize;

    // 儲存粒子的移動速度倍率
    float speed;

    // 儲存新粒子的基礎生命週期
    float lifetime;

    // 控制是否重新初始化所有粒子
    uint resetParticles;

    // 儲存產生隨機數時使用的 Seed
    uint randomSeed;
};

// 將輸入整數經過多次位元運算與乘法混合產生 Hash 值
uint Hash(uint value)
{
    // 將高位元資訊混合至低位元
    value ^= value >> 16;

    // 使用常數乘法進一步打散位元分布
    value *= 0x7feb352du;

    // 再次混合不同區段的位元
    value ^= value >> 15;

    // 使用另一組常數降低 Hash 結果的規律性
    value *= 0x846ca68bu;

    // 最後再次混合高低位元
    value ^= value >> 16;

    // 回傳完成混合後的 Hash 值
    return value;
}

// 根據傳入狀態產生 0 到 1 之間的偽隨機浮點數
float Random01(inout uint state)
{
    // 更新隨機狀態避免連續取得相同結果
    state = Hash(state);

    // 取低 24 位元並正規化至 0 到 1 的範圍
    return (state & 0x00ffffffu) / 16777216.0f;
}

// 根據粒子索引建立具有隨機位置與方向的新粒子
Particle CreateParticle(uint particleIndex)
{
    // 結合粒子索引與全域 Seed 建立此粒子的初始隨機狀態
    uint state = Hash(particleIndex ^ randomSeed);

    // 產生每個分量皆位於負 1 到正 1 的隨機位置
    float3 position = float3(Random01(state), Random01(state), Random01(state)) * 2.0f - 1.0f;

    // 產生每個分量皆位於負 1 到正 1 的隨機移動方向
    float3 direction = float3(Random01(state), Random01(state), Random01(state)) * 2.0f - 1.0f;

    // 檢查方向向量的平方長度是否過度接近零
    if (dot(direction, direction) < 0.0001f)

        // 使用向上的單位方向避免 normalize 產生不穩定結果
        direction = float3(0.0f, 1.0f, 0.0f);

    // 建立準備初始化的粒子資料
    Particle particle;

    // 將隨機位置限制在邊界大小的百分之八十範圍內
    particle.position = position * boundarySize * 0.8f;

    // 將生命週期隨機設定為基礎生命週期的百分之二十五到百分之一百
    particle.life = lifetime * (0.25f + Random01(state) * 0.75f);

    // 將隨機方向正規化後設為粒子的移動方向
    particle.velocity = normalize(direction);

    // 將用於記憶體對齊的欄位初始化為零
    particle.padding = 0.0f;

    // 回傳完成初始化的粒子
    return particle;
}

// 設定每個 Thread Group 使用 64 個 X 軸執行緒
[numthreads(64, 1, 1)]

// Compute Shader 的主要進入點
void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID)
{
    // 以目前 Dispatch Thread 的 X 編號作為粒子索引
    const uint particleIndex = dispatchThreadID.x;

    // Reset 時處理整個 Buffer 否則只處理目前有效粒子
    const uint dispatchCount = resetParticles != 0 ? particleCapacity : particleCount;

    // 排除因 Thread Group 向上取整而超出有效範圍的執行緒
    if (particleIndex >= dispatchCount)

        // 結束目前執行緒避免存取超出 Buffer 範圍
        return;

    // 判斷目前是否要求重新初始化粒子
    if (resetParticles != 0)
    {
        // 重新建立目前索引對應的粒子
        particles[particleIndex] = CreateParticle(particleIndex);

        // Reset 完成後不執行後續模擬
        return;
    }

    // 從 StructuredBuffer 讀取目前粒子的資料
    Particle particle = particles[particleIndex];

    // 根據方向速度與 Frame 時間更新粒子位置
    particle.position += particle.velocity * speed * deltaTime;

    // 檢查粒子是否超出 X 軸邊界
    if (particle.position.x > boundarySize || particle.position.x < -boundarySize)
    {
        // 將 X 座標限制回合法邊界範圍
        particle.position.x = clamp(particle.position.x, -boundarySize, boundarySize);

        // 反轉 X 軸速度形成碰撞反彈效果
        particle.velocity.x *= -1.0f;
    }

    // 檢查粒子是否超出 Y 軸邊界
    if (particle.position.y > boundarySize || particle.position.y < -boundarySize)
    {
        // 將 Y 座標限制回合法邊界範圍
        particle.position.y = clamp(particle.position.y, -boundarySize, boundarySize);

        // 反轉 Y 軸速度形成碰撞反彈效果
        particle.velocity.y *= -1.0f;
    }

    // 檢查粒子是否超出 Z 軸邊界
    if (particle.position.z > boundarySize || particle.position.z < -boundarySize)
    {
        // 將 Z 座標限制回合法邊界範圍
        particle.position.z = clamp(particle.position.z, -boundarySize, boundarySize);

        // 反轉 Z 軸速度形成碰撞反彈效果
        particle.velocity.z *= -1.0f;
    }

    // 扣除目前 Frame 經過的時間
    particle.life -= deltaTime;

    // 檢查粒子的生命週期是否已結束
    if (particle.life <= 0.0f)
    {
        // 使用額外混合過的索引重新產生粒子避免固定重複相同序列
        particle = CreateParticle(particleIndex + randomSeed * 1664525u);

        // 將重新生成的粒子移動至世界原點
        particle.position = float3(0.0f, 0.0f, 0.0f);
    }

    // 將更新完成的粒子資料寫回 StructuredBuffer
    particles[particleIndex] = particle;
}
//particle_render.hlsl

#include "particle_common.hlsli"

// 宣告供渲染階段唯讀使用的粒子 StructuredBuffer 並綁定至 t0
StructuredBuffer<Particle> particles : register(t0);

// 宣告相機與粒子渲染參數的常數緩衝區並綁定至 b0
cbuffer CameraConstants : register(b0)
{
    // 儲存 View Matrix 與 Projection Matrix 合併後的矩陣
    float4x4 viewProjection;

    // 儲存相機在世界空間中的右方向
    float3 cameraRight;

    // 儲存 Billboard 粒子的尺寸
    float particleSize;

    // 儲存相機在世界空間中的上方向
    float3 cameraUp;

    // 保留欄位以符合 Constant Buffer 的記憶體對齊需求
    float padding;

    // 儲存所有粒子使用的 RGBA 顏色
    float4 particleColor;
};

// 定義 Vertex Shader 傳遞給 Pixel Shader 的資料
struct VSOutput
{
    // 儲存經過座標轉換後的 Clip Space 位置
    float4 position : SV_POSITION;

    // 儲存傳遞給 Pixel Shader 的 UV 座標
    float2 uv : TEXCOORD0;
};

// Vertex Shader 的主要進入點
VSOutput VSMain(
    // 取得目前 Instance 中正在處理的頂點編號
    uint vertexID : SV_VertexID,

    // 取得目前正在繪製的 Instance 編號
    uint instanceID : SV_InstanceID)
{
    // 建立 Vertex Shader 的輸出資料
    VSOutput output;

    // 建立兩個三角形所需的六個 Billboard 頂點座標
    float2 corners[6] =
    {
        // 第一個三角形的左下角
        float2(-1.0f, -1.0f),

        // 第一個三角形的左上角
        float2(-1.0f, 1.0f),

        // 第一個三角形的右上角
        float2(1.0f, 1.0f),

        // 第二個三角形的左下角
        float2(-1.0f, -1.0f),

        // 第二個三角形的右上角
        float2(1.0f, 1.0f),

        // 第二個三角形的右下角
        float2(1.0f, -1.0f)
    };

    // 建立與六個 Billboard 頂點一一對應的 UV 座標
    float2 uvs[6] =
    {
        // 第一個三角形左下頂點使用左下 UV
        float2(0.0f, 1.0f),

        // 第一個三角形左上頂點使用左上 UV
        float2(0.0f, 0.0f),

        // 第一個三角形右上頂點使用右上 UV
        float2(1.0f, 0.0f),

        // 第二個三角形左下頂點使用左下 UV
        float2(0.0f, 1.0f),

        // 第二個三角形右上頂點使用右上 UV
        float2(1.0f, 0.0f),

        // 第二個三角形右下頂點使用右下 UV
        float2(1.0f, 1.0f)
    };

    // 使用 Instance ID 取得目前要繪製的粒子
    Particle particle =
        particles[instanceID];

    // 使用 Vertex ID 取得目前 Billboard 頂點的區域座標
    float2 corner =
        corners[vertexID];

    // 將粒子中心沿相機 Right 與 Up 方向展開成面向相機的 Billboard
    float3 worldPosition =
        particle.position
        + cameraRight
            * corner.x
            * particleSize
        + cameraUp
            * corner.y
            * particleSize;

    // 將 Billboard 頂點從 World Space 轉換到 Clip Space
    output.position =
        mul(
            float4(worldPosition, 1.0f),
            viewProjection);

    // 將目前頂點對應的 UV 傳遞至 Pixel Shader
    output.uv =
        uvs[vertexID];

    // 回傳完成計算的頂點輸出
    return output;
}

// Pixel Shader 的主要進入點
float4 PSMain(VSOutput input)
    : SV_TARGET
{
    // 將 UV 從 0 到 1 轉換成以中心為原點的負 1 到正 1 範圍
    float2 centeredUV =
        input.uv * 2.0f - 1.0f;

    // 計算目前 Pixel 與 Billboard 中心的距離
    float radius =
        length(centeredUV);

    // 根據距離產生從中心向外逐漸降低的透明度
    float alpha =
        saturate(
            1.0f - radius);

    // 排除圓形範圍外以及幾乎完全透明的 Pixel
    if (alpha <= 0.01f)
    {
        // 中止目前 Pixel 的輸出
        discard;
    }

    // 結合粒子顏色與計算後的透明度輸出最終顏色
    return float4(particleColor.rgb, particleColor.a * alpha);
}
//main.cpp

#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <directx/d3dx12_core.h>
#include <DirectXMath.h>
#include <exception>
#include <imgui.h>
#include <stdexcept>

#include "constant_buffer.h"
#include "graphics_engine.h"
#include "my_engine.h"
#include "pipeline_state.h"
#include "render_pass/render_state.h"
#include "root_signature.h"
#include "rw_structured_buffer.h"
#include "shader.h"
#include "skyline_debugger.h"
#include "system.h"
#include "ui/imgui_system.h"

namespace
{
// 定義粒子 Buffer 最多可容納十萬個粒子
constexpr UINT ParticleCapacity = 100000;

// 定義 Compute Shader 每個 Thread Group 的執行緒數量
constexpr UINT ComputeThreadCount = 64;

// 定義 CPU 端與 HLSL StructuredBuffer 對應的粒子資料結構
struct Particle
{
    // 儲存粒子的世界空間位置
    DirectX::XMFLOAT3 position;

    // 儲存粒子的剩餘生命時間
    float life;

    // 儲存粒子的移動方向
    DirectX::XMFLOAT3 velocity;

    // 保留欄位以維持與 HLSL 相同的記憶體配置
    float padding;
};

// 定義傳送給 Compute Shader 的粒子模擬參數
struct ParticleSimulationConstants
{
    // 儲存目前 Frame 經過的時間
    float deltaTime;

    // 儲存目前實際使用的粒子數量
    UINT particleCount;

    // 儲存粒子 Buffer 的最大容量
    UINT particleCapacity;

    // 儲存粒子活動空間的邊界大小
    float boundarySize;

    // 儲存粒子的移動速度
    float speed;

    // 儲存粒子的基礎生命週期
    float lifetime;

    // 控制是否重新初始化所有粒子
    UINT reset;

    // 儲存粒子隨機生成使用的 Seed
    UINT randomSeed;
};

// 定義傳送給 Graphics Shader 的相機與粒子渲染參數
struct CameraConstants
{
    // 儲存 View Matrix 與 Projection Matrix 合併後的矩陣
    DirectX::XMFLOAT4X4 viewProjection;

    // 儲存相機在世界空間中的右方向
    DirectX::XMFLOAT3 cameraRight;

    // 儲存 Billboard 粒子的尺寸
    float particleSize;

    // 儲存相機在世界空間中的上方向
    DirectX::XMFLOAT3 cameraUp;

    // 保留欄位以符合 Constant Buffer 對齊需求
    float padding;

    // 儲存粒子的 RGBA 顏色
    DirectX::XMFLOAT4 color;
};

// 編譯期間確認 Particle 大小與 HLSL StructuredBuffer 配置一致
static_assert(sizeof(Particle) == 32, "Particle must match the HLSL structured-buffer layout.");

// 編譯期間確認粒子模擬 Constant Buffer 的記憶體配置一致
static_assert(sizeof(ParticleSimulationConstants) == 32,
              "ParticleSimulationConstants must match the HLSL constant-buffer layout.");

// 編譯期間確認相機 Constant Buffer 的記憶體配置一致
static_assert(sizeof(CameraConstants) == 112, "CameraConstants must match the HLSL constant-buffer layout.");

// 建立粒子繪製使用的 Graphics Pipeline State 描述
D3D12_GRAPHICS_PIPELINE_STATE_DESC createGraphicsPipelineDescription(
    // 傳入 Graphics Pipeline 使用的 Root Signature 與已編譯 Shader
    ID3D12RootSignature* rootSignature, ID3DBlob* vertexShader, ID3DBlob* pixelShader)
{
    // 確認 Root Signature 已正確建立
    if (rootSignature == nullptr)

        // 缺少 Root Signature 時拋出參數錯誤
        throw std::invalid_argument("DayX_ComputeShader: A root signature is required.");

    // 確認 Vertex Shader 與 Pixel Shader 都已成功編譯
    if (vertexShader == nullptr || pixelShader == nullptr)

        // 缺少必要 Shader 時拋出參數錯誤
        throw std::invalid_argument("DayX_ComputeShader: Compiled graphics shaders are required.");

    // 建立並清零 Graphics Pipeline State 描述
    D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};

    // 不使用傳統 Vertex Buffer Input Layout
    description.InputLayout = {nullptr, 0};

    // 指定 Graphics Pipeline 使用的 Root Signature
    description.pRootSignature = rootSignature;

    // 指定已編譯的 Vertex Shader Bytecode
    description.VS = CD3DX12_SHADER_BYTECODE(vertexShader);

    // 指定已編譯的 Pixel Shader Bytecode
    description.PS = CD3DX12_SHADER_BYTECODE(pixelShader);

    // 使用 Direct3D 12 預設 Rasterizer 設定
    description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);

    // 關閉 Back-face Culling 讓 Billboard 正反面都能顯示
    description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;

    // 使用 Direct3D 12 預設 Blend State 作為基礎設定
    description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);

    // 取得第一個 Render Target 的 Blend 設定
    D3D12_RENDER_TARGET_BLEND_DESC& renderTarget = description.BlendState.RenderTarget[0];

    // 開啟 Alpha Blending
    renderTarget.BlendEnable = TRUE;

    // 將來源顏色乘上來源 Alpha
    renderTarget.SrcBlend = D3D12_BLEND_SRC_ALPHA;

    // 將目標顏色乘上一減來源 Alpha
    renderTarget.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;

    // 將來源與目標顏色相加
    renderTarget.BlendOp = D3D12_BLEND_OP_ADD;

    // Alpha 通道的來源值保持原值
    renderTarget.SrcBlendAlpha = D3D12_BLEND_ONE;

    // Alpha 通道的目標值乘上一減來源 Alpha
    renderTarget.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA;

    // 將來源與目標 Alpha 相加
    renderTarget.BlendOpAlpha = D3D12_BLEND_OP_ADD;

    // 允許寫入所有 RGBA 顏色通道
    renderTarget.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;

    // 使用預設 Depth Stencil 設定作為基礎
    description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);

    // 關閉 Depth Buffer 寫入避免半透明粒子互相遮蔽
    description.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;

    // 關閉 Stencil Test
    description.DepthStencilState.StencilEnable = FALSE;

    // 啟用所有 Sample
    description.SampleMask = UINT_MAX;

    // 指定 Pipeline 使用 Triangle 類型的 Primitive
    description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;

    // 指定只使用一個 Render Target
    description.NumRenderTargets = 1;

    // 指定 Render Target 的像素格式
    description.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;

    // 指定 Depth Stencil Buffer 使用 32-bit Float 格式
    description.DSVFormat = DXGI_FORMAT_D32_FLOAT;

    // 指定不使用 MSAA
    description.SampleDesc.Count = 1;

    // 回傳完成設定的 Graphics Pipeline 描述
    return description;
}

// 建立粒子模擬使用的 Compute Pipeline State 描述
D3D12_COMPUTE_PIPELINE_STATE_DESC createComputePipelineDescription(
    // 傳入 Compute Pipeline 使用的 Root Signature 與 Compute Shader
    ID3D12RootSignature* rootSignature, ID3DBlob* computeShader)
{
    // 確認 Root Signature 已正確建立
    if (rootSignature == nullptr)
        throw std::invalid_argument("DayX_ComputeShader: A root signature is required.");

    // 確認 Compute Shader 已成功編譯
    if (computeShader == nullptr)
        throw std::invalid_argument("DayX_ComputeShader: A compiled compute shader is required.");

    // 建立並清零 Compute Pipeline State 描述
    D3D12_COMPUTE_PIPELINE_STATE_DESC description{};

    // 指定 Compute Pipeline 使用的 Root Signature
    description.pRootSignature = rootSignature;

    // 指定已編譯的 Compute Shader Bytecode
    description.CS = CD3DX12_SHADER_BYTECODE(computeShader);

    // 回傳完成設定的 Compute Pipeline 描述
    return description;
}

// 根據 Frame Buffer 尺寸建立相機 Constant Buffer 資料
CameraConstants createCameraConstants(UINT width, UINT height)
{
    // 防止除以零以及建立無效的 Projection Matrix
    if (width == 0 || height == 0)
        throw std::invalid_argument("DayX_ComputeShader: Frame-buffer dimensions must be non-zero.");

    // 計算畫面的寬高比
    const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);

    // 將相機位置設定在世界原點後方
    const DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, 0.0f, -22.0f, 1.0f);

    // 將相機目標設定為世界原點
    const DirectX::XMVECTOR target = DirectX::XMVectorZero();

    // 定義相機的上方向
    const DirectX::XMVECTOR up = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

    // 建立 Left-handed View Matrix
    const DirectX::XMMATRIX view = DirectX::XMMatrixLookAtLH(eye, target, up);

    // 建立 45 度垂直視角的 Perspective Projection Matrix
    const DirectX::XMMATRIX projection =
        DirectX::XMMatrixPerspectiveFovLH(DirectX::XM_PIDIV4, aspectRatio, 0.1f, 100.0f);

    // 建立並清零相機 Constant Buffer 資料
    CameraConstants constants{};

    // 合併 View 與 Projection Matrix 並轉置後存入 Constant Buffer
    DirectX::XMStoreFloat4x4(&constants.viewProjection, DirectX::XMMatrixTranspose(view * projection));

    // 設定 Billboard 使用的相機右方向
    constants.cameraRight = {1.0f, 0.0f, 0.0f};

    // 設定 Billboard 使用的相機上方向
    constants.cameraUp = {0.0f, 1.0f, 0.0f};

    // 設定粒子的初始 Billboard 尺寸
    constants.particleSize = 0.06f;

    // 設定粒子的初始顏色與透明度
    constants.color = {0.2f, 0.65f, 1.0f, 0.85f};

    // 回傳完成初始化的相機參數
    return constants;
}
}

// Windows GUI 應用程式的進入點
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, LPWSTR commandLine, int showCommand)
{
    try
    {
        // 初始化 Skyline Debugger
        SkylineDebugger::Initialize("DayX_ComputeShader");

        // 建立應用程式視窗
        initWindow(instance, previous, commandLine, showCommand, TEXT("Compute Shader Particles"));

        // 確認視窗建立成功
        if (g_hWnd == nullptr)
            throw std::runtime_error("DayX_ComputeShader: Failed to create the application window.");

        // 建立 Graphics Engine
        GraphicsEngine graphicsEngine;

        // 初始化 Direct3D 12 與 Frame Buffer
        if (!graphicsEngine.init(g_hWnd, FRAME_BUFFER_W, FRAME_BUFFER_H))
            throw std::runtime_error("DayX_ComputeShader: Failed to initialize the graphics engine.");

        // 將目前使用的 D3D12 Device 提供給 Debugger
        SkylineDebugger::ConfigureD3D12(graphicsEngine.getD3DDevice());

        // 建立 Root Signature 管理物件
        RootSignature rootSignature;

        // 初始化 Root Signature 與預設 Sampler 設定
        if (!rootSignature.init(D3D12_FILTER_MIN_MAG_MIP_LINEAR, D3D12_TEXTURE_ADDRESS_MODE_CLAMP,
                                D3D12_TEXTURE_ADDRESS_MODE_CLAMP, D3D12_TEXTURE_ADDRESS_MODE_CLAMP))
            throw std::runtime_error("DayX_ComputeShader: Failed to initialize the root signature.");

        // 建立 Compute Shader 物件
        Shader computeShader;

        // 建立 Vertex Shader 物件
        Shader vertexShader;

        // 建立 Pixel Shader 物件
        Shader pixelShader;

        // 編譯並載入粒子模擬用 Compute Shader
        computeShader.loadCS("assets/shaders/particle_compute.hlsl", "CSMain");

        // 編譯並載入粒子渲染用 Vertex Shader
        vertexShader.loadVS("assets/shaders/particle_render.hlsl", "VSMain");

        // 編譯並載入粒子渲染用 Pixel Shader
        pixelShader.loadPS("assets/shaders/particle_render.hlsl", "PSMain");

        // 建立 Compute Pipeline State
        PipelineState computePipelineState;

        // 建立 Graphics Pipeline State
        PipelineState graphicsPipelineState;

        // 使用 Compute Shader 建立 Compute Pipeline State
        computePipelineState.init(
            createComputePipelineDescription(rootSignature.get(), computeShader.getCompiledBlob()));

        // 使用 Vertex Shader 與 Pixel Shader 建立 Graphics Pipeline State
        graphicsPipelineState.init(createGraphicsPipelineDescription(
            rootSignature.get(), vertexShader.getCompiledBlob(), pixelShader.getCompiledBlob()));

        // 建立並清零粒子模擬參數
        ParticleSimulationConstants simulationConstants{};

        // 設定初始粒子數量為一萬
        simulationConstants.particleCount = 10000;

        // 設定粒子 Buffer 最大容量
        simulationConstants.particleCapacity = ParticleCapacity;

        // 設定粒子活動空間邊界
        simulationConstants.boundarySize = 7.0f;

        // 設定粒子移動速度
        simulationConstants.speed = 2.0f;

        // 設定粒子的基礎生命週期
        simulationConstants.lifetime = 8.0f;

        // 要求第一次執行時初始化所有粒子
        simulationConstants.reset = 1;

        // 設定初始隨機 Seed
        simulationConstants.randomSeed = 1337;

        // 根據目前 Frame Buffer 尺寸建立相機參數
        CameraConstants cameraConstants = createCameraConstants(
            graphicsEngine.getFrameBufferWidth(), graphicsEngine.GetFrameBufferHeight());

        // 建立粒子模擬使用的 Constant Buffer
        ConstantBuffer simulationConstantBuffer;

        // 建立相機與粒子渲染使用的 Constant Buffer
        ConstantBuffer cameraConstantBuffer;

        // 初始化粒子模擬 Constant Buffer
        simulationConstantBuffer.init(sizeof(simulationConstants), &simulationConstants);

        // 初始化相機 Constant Buffer
        cameraConstantBuffer.init(sizeof(cameraConstants), &cameraConstants);

        // 建立可同時作為 UAV 與 SRV 使用的 StructuredBuffer
        RWStructuredBuffer particleBuffer;

        // 配置可容納十萬個 Particle 的 GPU Buffer
        particleBuffer.Init(sizeof(Particle), ParticleCapacity, nullptr);

        // 建立負責綁定 Shader Resource 的 Render State
        RenderState renderState;

        // 使用目前 D3D12 Device 初始化 Render State
        renderState.init(graphicsEngine.getD3DDevice());

        // 將粒子 Buffer 設定到對應的 UAV 與 SRV Slot
        renderState.setRWStructuredBuffer(0, 0, particleBuffer);

        // 取得 Graphics Engine 使用的 Render Context
        RenderContext& renderContext = graphicsEngine.getRenderContext();

        // 將目前 Command List 設定給 Render Context
        renderContext.SetCommandList(graphicsEngine.getCommandList());

        // 建立 ImGui 系統
        Skyline::UI::ImGuiSystem imguiSystem;

        // 使用目前視窗與 Graphics Engine 初始化 ImGui
        imguiSystem.initialize(g_hWnd, graphicsEngine);

        // 控制是否暫停粒子模擬
        bool paused = false;

        // 讓每個 Frame Buffer 都至少執行一次粒子 Reset
        int resetFramesRemaining = GraphicsEngine::FRAME_BUFFER_COUNT;

        // 分別保存每個 Back Buffer 上一次更新的時間
        std::array<std::chrono::steady_clock::time_point, GraphicsEngine::FRAME_BUFFER_COUNT> previousTimes{};

        // 取得應用程式開始時的時間點
        const auto initialTime = std::chrono::steady_clock::now();

        // 將所有 Back Buffer 的初始時間設定為相同值
        previousTimes.fill(initialTime);

        // 持續處理 Windows Message 直到應用程式關閉
        while (dispatchWindowMessage())
        {
            // 開始記錄目前 Frame 的 Rendering Command
            graphicsEngine.beginRender();

            // 開始建立目前 Frame 的 ImGui UI
            imguiSystem.beginFrame();

            // 將 UINT 粒子數量轉成 ImGui SliderInt 可使用的 int
            int displayedParticleCount = static_cast<int>(simulationConstants.particleCount);

            // 建立粒子控制用 ImGui 視窗
            if (imguiSystem.beginWindow("Compute shader particles"))
            {
                // 顯示粒子完全由 GPU 模擬的說明文字
                ImGui::TextUnformatted("Particle motion is simulated entirely on the GPU.");

                // 插入 UI 分隔線
                ImGui::Separator();

                // 建立使用對數刻度的粒子數量 Slider
                ImGui::SliderInt("Particle count", &displayedParticleCount, 1,
                                 static_cast<int>(ParticleCapacity), "%d", ImGuiSliderFlags_Logarithmic);

                // 建立調整粒子活動範圍的 Slider
                ImGui::SliderFloat("Spread range", &simulationConstants.boundarySize, 1.0f, 12.0f, "%.1f");

                // 建立調整粒子移動速度的 Slider
                ImGui::SliderFloat("Movement speed", &simulationConstants.speed, 0.0f, 8.0f, "%.2f");

                // 建立調整粒子生命週期的 Slider
                ImGui::SliderFloat("Lifetime", &simulationConstants.lifetime, 1.0f, 20.0f, "%.1f s");

                // 建立調整粒子 Billboard 尺寸的 Slider
                ImGui::SliderFloat("Particle size", &cameraConstants.particleSize, 0.01f, 0.25f, "%.3f");

                // 建立調整粒子 RGBA 顏色的 Color Picker
                ImGui::ColorEdit4("Particle color", &cameraConstants.color.x);

                // 建立暫停或恢復模擬的 Checkbox
                ImGui::Checkbox("Pause simulation", &paused);

                // 建立重新產生所有粒子的按鈕
                if (ImGui::Button("Reset particles"))
                {
                    // 改變 Seed 讓重新產生的粒子分布不同
                    ++simulationConstants.randomSeed;

                    // 要求所有 Frame Buffer 接下來都執行一次 Reset
                    resetFramesRemaining = GraphicsEngine::FRAME_BUFFER_COUNT;
                }

                // 將下一個 ImGui 元件放在同一行
                ImGui::SameLine();

                // 顯示目前 Compute Shader 需要 Dispatch 的 Thread Group 數量
                ImGui::Text("Dispatch groups: %u",
                            (simulationConstants.particleCount + ComputeThreadCount - 1) / ComputeThreadCount);

                // 顯示 ImGui 計算出的目前 Frame Rate
                ImGui::Text("Frame rate: %.1f FPS", ImGui::GetIO().Framerate);
            }

            // 結束目前 ImGui 視窗
            imguiSystem.endWindow();

            // 將 UI 修改後的粒子數量寫回模擬參數
            simulationConstants.particleCount = static_cast<UINT>(displayedParticleCount);

            // 取得目前正在使用的 Back Buffer 索引
            const UINT frameIndex = graphicsEngine.getBackBufferIndex();

            // 取得目前時間
            const auto currentTime = std::chrono::steady_clock::now();

            // 計算此 Back Buffer 距離上次使用時經過的秒數
            const float elapsed = std::chrono::duration<float>(currentTime - previousTimes[frameIndex]).count();

            // 更新目前 Back Buffer 的上一個時間點
            previousTimes[frameIndex] = currentTime;

            // 暫停時將 Delta Time 設為零否則限制最大值為 0.05 秒
            simulationConstants.deltaTime = paused ? 0.0f : (std::min)(elapsed, 0.05f);

            // 根據剩餘 Reset Frame 數量決定是否初始化粒子
            simulationConstants.reset = resetFramesRemaining > 0 ? 1u : 0u;

            // 將最新粒子模擬參數複製到 GPU Constant Buffer
            simulationConstantBuffer.copyToVRAM(simulationConstants);

            // 將最新相機與渲染參數複製到 GPU Constant Buffer
            cameraConstantBuffer.copyToVRAM(cameraConstants);

            // 設定 Compute Shader 使用的 Root Signature
            renderContext.setComputeRootSignature(rootSignature);

            // 切換至 Compute Pipeline State
            renderContext.setPipelineState(computePipelineState);

            // 將粒子模擬 Constant Buffer 綁定至 Compute Shader 的 b0
            renderState.setConstantBuffer(renderContext, ShaderStage::Compute, 0, simulationConstantBuffer);

            // 將目前 Frame 所需的 Compute Shader Resource 綁定至 Command List
            renderState.applyCompute(renderContext, frameIndex);

            // Reset 時處理整個 Buffer 否則只處理目前有效粒子
            const UINT dispatchedParticles = simulationConstants.reset != 0
                                                 ? simulationConstants.particleCapacity
                                                 : simulationConstants.particleCount;

            // 根據粒子數量計算 Thread Group 數量並執行 Compute Shader
            renderContext.dispatch((dispatchedParticles + ComputeThreadCount - 1) / ComputeThreadCount, 1, 1);

            // 取得粒子 StructuredBuffer 對應的 D3D12 Resource
            ID3D12Resource* particleResource = particleBuffer.getD3DResource();

            // 建立 UAV Barrier 描述
            D3D12_RESOURCE_BARRIER uavBarrier{};

            // 指定此 Barrier 為 UAV Barrier
            uavBarrier.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;

            // 指定需要同步 Compute Shader UAV 寫入的粒子 Resource
            uavBarrier.UAV.pResource = particleResource;

            // 確保之前的 UAV 寫入完成後才執行後續操作
            renderContext.resourceBarrier(uavBarrier);

            // 建立將粒子 Buffer 從 UAV 切換成 Shader Resource 的 Transition Barrier
            D3D12_RESOURCE_BARRIER toShaderResource{};

            // 指定此 Barrier 為 Resource State Transition
            toShaderResource.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;

            // 指定需要切換狀態的粒子 Resource
            toShaderResource.Transition.pResource = particleResource;

            // 指定切換前為 Compute Shader 可寫入的 UAV 狀態
            toShaderResource.Transition.StateBefore = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;

            // 指定切換後允許 Vertex 與 Pixel Shader 讀取
            toShaderResource.Transition.StateAfter =
                D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;

            // 對 Resource 的所有 Subresource 套用狀態切換
            toShaderResource.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;

            // 執行 UAV 到 Shader Resource 的狀態切換
            renderContext.resourceBarrier(toShaderResource);

            // 設定 Graphics Pipeline 使用的 Root Signature
            renderContext.setRootSignature(rootSignature);

            // 切換至粒子渲染用 Graphics Pipeline State
            renderContext.setPipelineState(graphicsPipelineState);

            // 指定使用 Triangle List 繪製粒子 Billboard
            renderContext.setPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

            // 將相機 Constant Buffer 綁定至 Graphics Shader 的 b0
            renderState.setConstantBuffer(renderContext, ShaderStage::Graphics, 0, cameraConstantBuffer);

            // 將目前 Frame 所需的 Graphics Shader Resource 綁定至 Command List
            renderState.applyGraphics(renderContext, frameIndex);

            // 每個粒子以六個頂點繪製兩個三角形並透過 Instance 數量一次繪製所有粒子
            graphicsEngine.getCommandList()->DrawInstanced(6, simulationConstants.particleCount, 0, 0);

            // 建立將粒子 Buffer 切回 UAV 狀態的 Transition Barrier
            D3D12_RESOURCE_BARRIER toUnorderedAccess{};

            // 指定此 Barrier 為 Resource State Transition
            toUnorderedAccess.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;

            // 指定要切換狀態的粒子 Resource
            toUnorderedAccess.Transition.pResource = particleResource;

            // 指定切換前為 Graphics Shader 可讀取狀態
            toUnorderedAccess.Transition.StateBefore =
                D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;

            // 指定切換後回到 Compute Shader 可寫入的 UAV 狀態
            toUnorderedAccess.Transition.StateAfter = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;

            // 對 Resource 的所有 Subresource 套用狀態切換
            toUnorderedAccess.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;

            // 執行 Shader Resource 到 UAV 的狀態切換
            renderContext.resourceBarrier(toUnorderedAccess);

            // 檢查是否仍需要在其他 Frame Buffer 執行 Reset
            if (resetFramesRemaining > 0)

                // 完成一個 Frame 的 Reset 後減少剩餘次數
                --resetFramesRemaining;

            // 將目前 Frame 的 ImGui Draw Data 記錄到 Command List
            imguiSystem.render();

            // 完成目前 Frame 並提交 Rendering Command
            graphicsEngine.endRender();

            // 將 D3D12 Debug Layer 訊息輸出至 Skyline Debugger
            SkylineDebugger::LogD3D12Messages(graphicsEngine.getD3DDevice());
        }

        // 等待 GPU 完成所有尚未執行完畢的 Rendering Command
        graphicsEngine.waitDraw();

        // 關閉 ImGui 並釋放相關 Resource
        imguiSystem.shutdown();

        // 關閉 Skyline Debugger
        SkylineDebugger::Shutdown();

        // 正常結束程式
        return EXIT_SUCCESS;
    }

    // 捕捉標準例外並輸出錯誤資訊
    catch (const std::exception& error)
    {
        // 顯示包含例外內容的致命錯誤視窗
        SkylineDebugger::ShowFatalError("DayX_ComputeShader initialization failed", error);

        // 關閉 Skyline Debugger
        SkylineDebugger::Shutdown();

        // 回傳程式執行失敗
        return EXIT_FAILURE;
    }

    // 捕捉無法識別類型的其他例外
    catch (...)
    {
        // 顯示未知致命錯誤訊息
        SkylineDebugger::ShowFatalError(
            "DayX_ComputeShader initialization failed", "An unknown fatal error occurred.");

        // 關閉 Skyline Debugger
        SkylineDebugger::Shutdown();

        // 回傳程式執行失敗
        return EXIT_FAILURE;
    }
}

結果

Yes


上一篇
Day 19 : ImGui
下一篇
Day 21:體積雲實作 1 - Noise、FBM
系列文
因為 AI 看不懂老舊程式,只好乖乖從零開始學 DirectX 12 與 HLSL23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言